# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.


##################
#Bugzilla Test 12#
######Errors######

use 5.14.0;
use strict;
use warnings;

use lib qw(. lib local/lib/perl5 t);

use Bugzilla::Constants;
use Bugzilla::WebService::Constants;

use File::Spec;
use Support::Files;
use Support::Templates;
use Test::More;

my %Errors = ();

# Just a workaround for template errors handling. Define it as used.
push @{$Errors{code}{template_error}{used_in}{'Bugzilla/Error.pm'}}, 0;

# Define files to test. Each file would have a list of error messages, if any.
my %test_templates = ();
my %test_modules   = ();

# Find all modules
foreach my $module (@Support::Files::testitems) {
  $test_modules{$module} = ();
}

# Find all error templates
# Process all files since otherwise handling template hooks would became too
# hairy. But let us do it only once.

foreach my $include_path (@include_paths) {
  foreach my $path (@{$actual_files{$include_path}}) {
    my $file = File::Spec->catfile($include_path, $path);
    $file =~ s/\s.*$//;    # nuke everything after the first space
    $file =~ s|\\|/|g if ON_WINDOWS;    # convert \ to / in path if on windows
    $test_templates{$file} = () if $file =~ m#global/(code|user)-error\.html\.tmpl#;

    # Make sure the extension is not disabled
    if ($file =~ m#^(extensions/[^/]+/)#) {
      $test_templates{$file} = ()
        if !-e "${1}disabled"
        && $file =~ m#global/(code|user)-error-errors\.html\.tmpl#;
    }
  }
}

# Count the tests. The +1 is for checking the WS_ERROR_CODE errors.
my $tests = (scalar keys %test_modules) + (scalar keys %test_templates) + 1;
exit 0 if !$tests;

# Set requested tests counter.
plan tests => $tests;

# Collect all errors defined in templates
foreach my $file (keys %test_templates) {
  $file =~ m|template/([^/]+).*/global/([^/]+)-error(?:-errors)?\.html\.tmpl|;
  my $lang    = $1;
  my $errtype = $2;

  if (!open(TMPL, $file)) {
    Register(\%test_templates, $file, "could not open file --WARNING");
    next;
  }

  my $lineno = 0;
  while (my $line = <TMPL>) {
    $lineno++;
    if ($line =~ /\[%\s[A-Z]+\s*error\s*==\s*"(.+)"\s*%\]/) {
      my $errtag = $1;
      if ($errtag =~ /\s/) {
        Register(\%test_templates, $file,
              "has an error definition \"$errtag\" at line $lineno with "
            . "space(s) embedded --ERROR");
      }
      else {
        push @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}}, $lineno;
      }
    }
  }
  close(TMPL);
}

# Collect all used errors from cgi/pm files
foreach my $file (keys %test_modules) {
  $file =~ s/\s.*$//;    # nuke everything after the first space (#comment)
  next if (!$file);      # skip null entries
  if (!open(TMPL, $file)) {
    Register(\%test_modules, $file, "could not open file --WARNING");
    next;
  }

  my $lineno = 0;
  while (my $line = <TMPL>) {
    last if $line =~ /^__END__/;    # skip the POD (at least in
                                    # Bugzilla/Error.pm)
    $lineno++;
    if ($line
      =~ /^[^#]*\b(Throw(Code|User)Error|(user_)?error\s+=>)\s*\(?\s*["'](.*?)['"]/)
    {
      my $errtype;

      # If it's a normal ThrowCode/UserError
      if ($2) {
        $errtype = lc($2);
      }

      # If it's an AUTH_ERROR tag
      else {
        $errtype = $3 ? 'user' : 'code';
      }
      my $errtag = $4;
      push @{$Errors{$errtype}{$errtag}{used_in}{$file}}, $lineno;
    }
  }

  close(TMPL);
}

# Now let us start the checks

foreach my $errtype (keys %Errors) {
  foreach my $errtag (keys %{$Errors{$errtype}}) {

    # Check for undefined tags
    if (!defined $Errors{$errtype}{$errtag}{defined_in}) {
      UsedIn($errtype, $errtag, "any");
    }
    else {
      # Check for all languages!!!
      my @langs = ();
      foreach my $lang (@languages) {
        if (!defined $Errors{$errtype}{$errtag}{defined_in}{$lang}) {
          push @langs, $lang;
        }
      }
      if (scalar @langs) {
        UsedIn($errtype, $errtag, join(', ', @langs));
      }

      # Now check for tag usage in all DEFINED languages
      foreach my $lang (keys %{$Errors{$errtype}{$errtag}{defined_in}}) {
        if (!defined $Errors{$errtype}{$errtag}{used_in}) {
          DefinedIn($errtype, $errtag, $lang);
        }
      }
    }
  }
}

# And make sure that everything defined in WS_ERROR_CODE
# is actually a valid error.
foreach my $err_name (keys %{WS_ERROR_CODE()}) {
  if (!defined $Errors{'code'}{$err_name} && !defined $Errors{'user'}{$err_name})
  {
    Register(\%test_modules, 'WS_ERROR_CODE',
          "Error tag '$err_name' is used in WS_ERROR_CODE in"
        . " Bugzilla/WebService/Constants.pm"
        . " but not defined in any template, and not used in any code.");
  }
}

# Now report modules results
foreach my $file (sort keys %test_modules) {
  Report($file, @{$test_modules{$file}});
}

# And report WS_ERROR_CODE results
Report('WS_ERROR_CODE', @{$test_modules{'WS_ERROR_CODE'}});

# Now report templates results
foreach my $file (sort keys %test_templates) {
  Report($file, @{$test_templates{$file}});
}

sub Register {
  my ($hash, $file, $message, $warning) = @_;

  # If set to 1, $warning will avoid the test to fail.
  $warning ||= 0;
  push(@{$hash->{$file}}, {'message' => $message, 'warning' => $warning});
}

sub Report {
  my ($file, @errors) = @_;
  if (scalar @errors) {

    # Do we only have warnings to report or also real errors?
    my @real_errors = grep { $_->{'warning'} == 0 } @errors;

    # Extract error messages.
    @errors = map { $_->{'message'} } @errors;
    if (scalar(@real_errors)) {
      ok(0, "$file has " . scalar(@errors) . " error(s):\n" . join("\n", @errors));
    }
    else {
      ok(1,
            "--WARNING $file has "
          . scalar(@errors)
          . " unused error tag(s):\n"
          . join("\n", @errors));
    }
  }
  else {
    # This is used for both code and template files, so let's use
    # file-independent phrase
    ok(1, "$file uses error tags correctly");
  }
}

sub UsedIn {
  my ($errtype, $errtag, $lang) = @_;
  $lang = $lang || "any";
  foreach my $file (keys %{$Errors{$errtype}{$errtag}{used_in}}) {
    Register(\%test_modules, $file,
          "$errtype error tag '$errtag' is used at line(s) ("
        . join(',', @{$Errors{$errtype}{$errtag}{used_in}{$file}})
        . ") but not defined for language(s): $lang");
  }
}

sub DefinedIn {
  my ($errtype, $errtag, $lang) = @_;
  foreach my $file (keys %{$Errors{$errtype}{$errtag}{defined_in}{$lang}}) {
    Register(
      \%test_templates,
      $file,
      "$errtype error tag '$errtag' is defined at line(s) ("
        . join(',', @{$Errors{$errtype}{$errtag}{defined_in}{$lang}{$file}})
        . ") but is not used anywhere",
      1
    );
  }
}

exit 0;
